<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Captcha
 * @subpackage Adapter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 * @version $Id: Image.php 24821 2012-05-29 14:54:50Z adamlundrigan $
 */
/**
 *
 * @see Zend_Captcha_Word
 */
require_once 'Zend/Captcha/Word.php';

/**
 * Image-based captcha element
 * Generates image displaying random word
 * @category Zend
 * @package Zend_Captcha
 * @subpackage Adapter
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Captcha_Image extends Zend_Captcha_Word {

    /**
     * Directory for generated images
     * @var string
     */
    protected $_imgDir = "./images/captcha/";

    /**
     * URL for accessing images
     * @var string
     */
    protected $_imgUrl = "/images/captcha/";

    /**
     * Image's alt tag content
     * @var string
     */
    protected $_imgAlt = "";

    /**
     * Image suffix (including dot)
     * @var string
     */
    protected $_suffix = ".png";

    /**
     * Image width
     * @var int
     */
    protected $_width = 200;

    /**
     * Image height
     * @var int
     */
    protected $_height = 50;

    /**
     * Font size
     * @var int
     */
    protected $_fsize = 24;

    /**
     * Image font file
     * @var string
     */
    protected $_font;

    /**
     * Image to use as starting point
     * Default is blank image.
     * If provided, should be PNG image.
     * @var string
     */
    protected $_startImage;

    /**
     * How frequently to execute garbage collection
     * @var int
     */
    protected $_gcFreq = 10;

    /**
     * How long to keep generated images
     * @var int
     */
    protected $_expiration = 600;

    /**
     * Number of noise dots on image
     * Used twice - before and after transform
     * @var int
     */
    protected $_dotNoiseLevel = 100;

    /**
     * Number of noise lines on image
     * Used twice - before and after transform
     * @var int
     */
    protected $_lineNoiseLevel = 5;

    /**
     *
     * @return string
     */
    public function getImgAlt () {
        return $this -> _imgAlt;
    }

    /**
     *
     * @return string
     */
    public function getStartImage () {
        return $this -> _startImage;
    }

    /**
     *
     * @return int
     */
    public function getDotNoiseLevel () {
        return $this -> _dotNoiseLevel;
    }

    /**
     *
     * @return int
     */
    public function getLineNoiseLevel () {
        return $this -> _lineNoiseLevel;
    }

    /**
     * Get captcha expiration
     * @return int
     */
    public function getExpiration () {
        return $this -> _expiration;
    }

    /**
     * Get garbage collection frequency
     * @return int
     */
    public function getGcFreq () {
        return $this -> _gcFreq;
    }

    /**
     * Get font to use when generating captcha
     * @return string
     */
    public function getFont () {
        return $this -> _font;
    }

    /**
     * Get font size
     * @return int
     */
    public function getFontSize () {
        return $this -> _fsize;
    }

    /**
     * Get captcha image height
     * @return int
     */
    public function getHeight () {
        return $this -> _height;
    }

    /**
     * Get captcha image directory
     * @return string
     */
    public function getImgDir () {
        return $this -> _imgDir;
    }

    /**
     * Get captcha image base URL
     * @return string
     */
    public function getImgUrl () {
        return $this -> _imgUrl;
    }

    /**
     * Get captcha image file suffix
     * @return string
     */
    public function getSuffix () {
        return $this -> _suffix;
    }

    /**
     * Get captcha image width
     * @return int
     */
    public function getWidth () {
        return $this -> _width;
    }

    /**
     *
     * @param string $startImage
     */
    public function setStartImage ($startImage) {
        $this -> _startImage = $startImage;
        return $this;
    }

    /**
     *
     * @param int $dotNoiseLevel
     */
    public function setDotNoiseLevel ($dotNoiseLevel) {
        $this -> _dotNoiseLevel = $dotNoiseLevel;
        return $this;
    }

    /**
     *
     * @param int $lineNoiseLevel
     */
    public function setLineNoiseLevel ($lineNoiseLevel) {
        $this -> _lineNoiseLevel = $lineNoiseLevel;
        return $this;
    }

    /**
     * Set captcha expiration
     * @param int $expiration
     * @return Zend_Captcha_Image
     */
    public function setExpiration ($expiration) {
        $this -> _expiration = $expiration;
        return $this;
    }

    /**
     * Set garbage collection frequency
     * @param int $gcFreq
     * @return Zend_Captcha_Image
     */
    public function setGcFreq ($gcFreq) {
        $this -> _gcFreq = $gcFreq;
        return $this;
    }

    /**
     * Set captcha font
     * @param string $font
     * @return Zend_Captcha_Image
     */
    public function setFont ($font) {
        $this -> _font = $font;
        return $this;
    }

    /**
     * Set captcha font size
     * @param int $fsize
     * @return Zend_Captcha_Image
     */
    public function setFontSize ($fsize) {
        $this -> _fsize = $fsize;
        return $this;
    }

    /**
     * Set captcha image height
     * @param int $height
     * @return Zend_Captcha_Image
     */
    public function setHeight ($height) {
        $this -> _height = $height;
        return $this;
    }

    /**
     * Set captcha image storage directory
     * @param string $imgDir
     * @return Zend_Captcha_Image
     */
    public function setImgDir ($imgDir) {
        $this -> _imgDir = rtrim ($imgDir, "/\\") . '/';
        return $this;
    }

    /**
     * Set captcha image base URL
     * @param string $imgUrl
     * @return Zend_Captcha_Image
     */
    public function setImgUrl ($imgUrl) {
        $this -> _imgUrl = rtrim ($imgUrl, "/\\") . '/';
        return $this;
    }

    /**
     *
     * @param string $imgAlt
     */
    public function setImgAlt ($imgAlt) {
        $this -> _imgAlt = $imgAlt;
        return $this;
    }

    /**
     * Set captch image filename suffix
     * @param string $suffix
     * @return Zend_Captcha_Image
     */
    public function setSuffix ($suffix) {
        $this -> _suffix = $suffix;
        return $this;
    }

    /**
     * Set captcha image width
     * @param int $width
     * @return Zend_Captcha_Image
     */
    public function setWidth ($width) {
        $this -> _width = $width;
        return $this;
    }

    /**
     * Generate random frequency
     * @return float
     */
    protected function _randomFreq () {
        return mt_rand (700000, 1000000) / 15000000;
    }

    /**
     * Generate random phase
     * @return float
     */
    protected function _randomPhase () {
        // random phase from 0 to pi
        return mt_rand (0, 3141592) / 1000000;
    }

    /**
     * Generate random character size
     * @return int
     */
    protected function _randomSize () {
        return mt_rand (300, 700) / 100;
    }

    /**
     * Generate captcha
     * @return string captcha ID
     */
    public function generate () {
        $id = parent::generate ();
        $tries = 5;
        // If there's already such file, try creating a new ID
        while ($tries --  && file_exists ($this -> getImgDir () . $id . $this -> getSuffix ())) {
            $id = $this -> _generateRandomId ();
            $this -> _setId ($id);
        }
        $this -> _generateImage ($id, $this -> getWord ());
        if (mt_rand (1, $this -> getGcFreq ()) == 1) {
            $this -> _gc ();
        }
        return $id;
    }

    /**
     * Generate image captcha
     * Override this function if you want different image generator
     * Wave transform from http://www.captcha.ru/captchas/multiwave/
     * @param string $id Captcha ID
     * @param string $word Captcha word
     */
    protected function _generateImage ($id, $word) {
        if ( ! extension_loaded ("gd")) {
            require_once 'Zend/Captcha/Exception.php';
            throw new Zend_Captcha_Exception ("Image CAPTCHA requires GD extension");
        }
        if ( ! function_exists ("imagepng")) {
            require_once 'Zend/Captcha/Exception.php';
            throw new Zend_Captcha_Exception ("Image CAPTCHA requires PNG support");
        }
        if ( ! function_exists ("imageftbbox")) {
            require_once 'Zend/Captcha/Exception.php';
            throw new Zend_Captcha_Exception ("Image CAPTCHA requires FT fonts support");
        }
        $font = $this -> getFont ();
        if (empty ($font)) {
            require_once 'Zend/Captcha/Exception.php';
            throw new Zend_Captcha_Exception ("Image CAPTCHA requires font");
        }
        $w = $this -> getWidth ();
        $h = $this -> getHeight ();
        $fsize = $this -> getFontSize ();
        $img_file = $this -> getImgDir () . $id . $this -> getSuffix ();
        if (empty ($this -> _startImage)) {
            $img = imagecreatetruecolor ($w, $h);
        } else {
            $img = imagecreatefrompng ($this -> _startImage);
            if ( ! $img) {
                require_once 'Zend/Captcha/Exception.php';
                throw new Zend_Captcha_Exception ("Can not load start image");
            }
            $w = imagesx ($img);
            $h = imagesy ($img);
        }
        $text_color = imagecolorallocate ($img, 0, 0, 0);
        $bg_color = imagecolorallocate ($img, 255, 255, 255);
        imagefilledrectangle ($img, 0, 0, $w - 1, $h - 1, $bg_color);
        $textbox = imageftbbox ($fsize, 0, $font, $word);
        $x = ($w - ($textbox[2] - $textbox[0])) / 2;
        $y = ($h - ($textbox[7] - $textbox[1])) / 2;
        imagefttext ($img, $fsize, 0, $x, $y, $text_color, $font, $word);
        // generate noise
        for ($i = 0; $i < $this -> _dotNoiseLevel; $i ++ ) {
            imagefilledellipse ($img, mt_rand (0, $w), mt_rand (0, $h), 2, 2, $text_color);
        }
        for ($i = 0; $i < $this -> _lineNoiseLevel; $i ++ ) {
            imageline ($img, mt_rand (0, $w), mt_rand (0, $h), mt_rand (0, $w), mt_rand (0, $h), $text_color);
        }
        // transformed image
        $img2 = imagecreatetruecolor ($w, $h);
        $bg_color = imagecolorallocate ($img2, 255, 255, 255);
        imagefilledrectangle ($img2, 0, 0, $w - 1, $h - 1, $bg_color);
        // apply wave transforms
        $freq1 = $this -> _randomFreq ();
        $freq2 = $this -> _randomFreq ();
        $freq3 = $this -> _randomFreq ();
        $freq4 = $this -> _randomFreq ();
        $ph1 = $this -> _randomPhase ();
        $ph2 = $this -> _randomPhase ();
        $ph3 = $this -> _randomPhase ();
        $ph4 = $this -> _randomPhase ();
        $szx = $this -> _randomSize ();
        $szy = $this -> _randomSize ();
        for ($x = 0; $x < $w; $x ++ ) {
            for ($y = 0; $y < $h; $y ++ ) {
                $sx = $x + (sin ($x * $freq1 + $ph1) + sin ($y * $freq3 + $ph3)) * $szx;
                $sy = $y + (sin ($x * $freq2 + $ph2) + sin ($y * $freq4 + $ph4)) * $szy;
                if ($sx < 0 || $sy < 0 || $sx >= $w - 1 || $sy >= $h - 1) {
                    continue;
                } else {
                    $color = (imagecolorat ($img, $sx, $sy) >> 16) & 0xFF;
                    $color_x = (imagecolorat ($img, $sx + 1, $sy) >> 16) & 0xFF;
                    $color_y = (imagecolorat ($img, $sx, $sy + 1) >> 16) & 0xFF;
                    $color_xy = (imagecolorat ($img, $sx + 1, $sy + 1) >> 16) & 0xFF;
                }
                if ($color == 255 && $color_x == 255 && $color_y == 255 && $color_xy == 255) {
                    // ignore background
                    continue;
                } elseif ($color == 0 && $color_x == 0 && $color_y == 0 && $color_xy == 0) {
                    // transfer inside of the image as-is
                    $newcolor = 0;
                } else {
                    // do antialiasing for border items
                    $frac_x = $sx - floor ($sx);
                    $frac_y = $sy - floor ($sy);
                    $frac_x1 = 1 - $frac_x;
                    $frac_y1 = 1 - $frac_y;
                    $newcolor = $color * $frac_x1 * $frac_y1 + $color_x * $frac_x * $frac_y1 + $color_y * $frac_x1 * $frac_y + $color_xy * $frac_x * $frac_y;
                }
                imagesetpixel ($img2, $x, $y, imagecolorallocate ($img2, $newcolor, $newcolor, $newcolor));
            }
        }
        // generate noise
        for ($i = 0; $i < $this -> _dotNoiseLevel; $i ++ ) {
            imagefilledellipse ($img2, mt_rand (0, $w), mt_rand (0, $h), 2, 2, $text_color);
        }
        for ($i = 0; $i < $this -> _lineNoiseLevel; $i ++ ) {
            imageline ($img2, mt_rand (0, $w), mt_rand (0, $h), mt_rand (0, $w), mt_rand (0, $h), $text_color);
        }
        imagepng ($img2, $img_file);
        imagedestroy ($img);
        imagedestroy ($img2);
    }

    /**
     * Remove old files from image directory
     */
    protected function _gc () {
        $expire = time () - $this -> getExpiration ();
        $imgdir = $this -> getImgDir ();
        if ( ! $imgdir || strlen ($imgdir) < 2) {
            // safety guard
            return;
        }
        $suffixLength = strlen ($this -> _suffix);
        foreach (new DirectoryIterator ($imgdir) as $file) {
            if ( ! $file -> isDot () &&  ! $file -> isDir ()) {
                if (file_exists ($file -> getPathname ()) && $file -> getMTime () < $expire) {
                    // only deletes files ending with $this->_suffix
                    if (substr ($file -> getFilename (),  - ($suffixLength)) == $this -> _suffix) {
                        unlink ($file -> getPathname ());
                    }
                }
            }
        }
    }

    /**
     * Display the captcha
     * @param Zend_View_Interface $view
     * @param mixed $element
     * @return string
     */
    public function render (Zend_View_Interface $view = null, $element = null) {
        $endTag = ' />';
        if (($view instanceof Zend_View_Abstract) &&  ! $view -> doctype () -> isXhtml ()) {
            $endTag = '>';
        }
        return '<img width="' . $this -> getWidth () . '" height="' . $this -> getHeight () . '" alt="' . $this -> getImgAlt () . '" src="' . $this -> getImgUrl () . $this -> getId () . $this -> getSuffix () . '"' . $endTag;
    }

}
